from contextlib import contextmanager
from khronos.utils import UNDEF

class DataStore(object):
    """Tabular data store. Stores data in text format and allows reading and writing data."""
    columns   = []    # list of Column objects
    separator = None  # column separator (None means whitespace-separated columns)
    header    = "# "  # header string
    
    def __init__(self, path, buffer_size=100, columns=UNDEF, separator=UNDEF, header=UNDEF):
        self.path = path
        self.file = None
        self.buffer = []
        self.buffer_size = buffer_size
        self.columns = list(columns if columns is not UNDEF else self.__class__.columns)
        self.separator = separator if separator is not UNDEF else self.__class__.separator
        self.header = header if header is not UNDEF else self.__class__.header
        
    def __repr__(self):
        path = self.path
        if len(path) > 50:
            path = "[...]" + path[-45:]
        return "<%s @ %08x - %s>" % (self.__class__.__name__, id(self), path)
        
    def __del__(self):
        self.flush()
        
    def open(self, mode="r"):
        self.file = open(self.path, mode)
        
    def close(self):
        self.file.close()
        
    @contextmanager
    def opened(self, *args, **kwargs):
        self.open(*args, **kwargs)
        yield
        self.close()
        
    def insert(self, data=(), extract_row=False, **kw_data):
        row = dict(data if not extract_row else self.extract_row(data))
        row.update(kw_data)
        self.buffer.append(self.format_row(row))
        if len(self.buffer) > self.buffer_size:
            self.flush()
            
    def insert_header(self):
        self.buffer.append(self.format_header())
        if len(self.buffer) > self.buffer_size:
            self.flush()
            
    def flush(self):
        if len(self.buffer) > 0:
            with self.opened(mode="a"):
                while len(self.buffer) > 0:
                    self.file.write(self.buffer[0] + "\n")
                    self.buffer.pop(0)
                    
    def format_row(self, row):
        indent = " " * len(self.header)
        separator = self.separator or " "
        return indent + separator.join(c.format(row[c.name]) for c in self.columns)
        
    def format_header(self):
        separator = self.separator or " "
        return self.header + separator.join(c.header() for c in self.columns)
        
    def extract_row(self, data):
        """Extract a row from the given data. The return value must be a dictionary whose keys 
        are column names and the values are the row's values. This method may be redefined in 
        subclasses."""
        return data
        
    def parse_row(self, line, columns):
        """Parse a row from a string and return a dictionary holding the row's data."""
        values = line.split(self.separator)
        if columns is None:
            return dict((c.name, c.type(v.strip())) 
                        for c, v in zip(self.columns, values))
        else:
            return dict((c.name, c.type(v.strip())) 
                        for c, v in zip(self.columns, values)
                        if c.name in columns)
            
    def __iter__(self):
        return self.read()
        
    def read(self, columns=None):
        """Reads all rows in the given store and returns a list of dictionaries."""
        if len(self.columns) == 0:
            self.guess()
        with self.opened("r"):
            for line in self.file:
                line = line.strip()
                if len(line) > 0 and not line.startswith(self.header):
                    yield self.parse_row(line, columns)
                    
    def filter(self, condition):
        return Filter(self.read(), condition)
        
    def guess(self):
        """Guess the column names from the first header line found in the file."""
        guessed = False
        with self.opened("r"):
            for line in self.file:
                if line.startswith(self.header):
                    for col_name in line[len(self.header):].split(self.separator):
                        self.columns.append(Column(col_name.strip()))
                    guessed = True
                    break
        if not guessed:
            raise ValueError("unable to guess column names. No header line present in file")
            
class Filter(list):
    """A simple filter of rows in a data store."""
    def __init__(self, data, condition):
        list.__init__(self)
        for row in data:
            if condition(row):
                self.append(row)
                
    def filter(self, condition):
        return Filter(self, condition)
        
class Column(object):
    """Column metadata class for use with tabular data stores."""
    def __init__(self, name, type=str, width=20):
        self.name = name
        self.type = type
        self.width = width
        
    def header(self):
        return self.format(self.name)
        
    def format(self, value):
        fmt = "%" + str(self.width) + "s"
        return fmt % (value,)
        
